﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;

namespace Loki.Data
{
    /// <summary>
    /// Tool module for database.
    /// </summary>
    public static class DatabaseUtils
    {
        #region Reader
        /// <summary>
        /// Gets the value from the database flow.
        /// </summary>
        /// <typeparam name="T">Returned type.</typeparam>
        /// <param name="P_Reader">The data record.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        /// <param name="P_DefaultFunc">The default value getter functor.</param>
        /// <param name="P_Parser">The parser fonctor.</param>
        /// <returns>Returns the getted value.</returns>
        public static T GetValue<T>(this IDataRecord P_Reader, string P_AttributeName, Func<T> P_DefaultFunc, Converter<object, T> P_Parser)
        {
            object L_Value = P_Reader[P_AttributeName];
            if (L_Value == DBNull.Value)
            {
                return P_DefaultFunc();
            }
            else
            {
                return P_Parser(L_Value);
            }
        }

        #region Date
        /// <summary>
        /// Gets a date value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static DateTime GetDate(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<DateTime>(
                P_Reader,
                P_AttributeName,
                () => default(DateTime).Date,
                delegate(object o)
                {
                    DateTime L_Buffer = Convert.ToDateTime(o, CultureInfo.InvariantCulture);
                    return new DateTime(L_Buffer.Year, L_Buffer.Month, L_Buffer.Day, 0, 0, 0, 0, DateTimeKind.Utc);
                });
        }

        /// <summary>
        /// Gets a nullable date value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The data record.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static DateTime? GetNullableDate(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<DateTime?>(
                P_Reader,
                P_AttributeName,
                () => null,
                delegate(object o)
                {
                    DateTime L_Buffer = Convert.ToDateTime(o, CultureInfo.InvariantCulture);
                    return new DateTime(L_Buffer.Year, L_Buffer.Month, L_Buffer.Day, 0, 0, 0, 0, DateTimeKind.Utc);
                });
        }

        /// <summary>
        /// Gets a datetime value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The data record.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static DateTime GetDateAndTime(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<DateTime>(P_Reader, P_AttributeName, () => default(DateTime), (x) => Convert.ToDateTime(x, CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Gets a nullabme datetime value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The data record.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static DateTime? GetNullableDateAndTime(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<DateTime?>(P_Reader, P_AttributeName, () => null, (x) => Convert.ToDateTime(x, CultureInfo.InvariantCulture));
        }
        #endregion

        #region Boolean
        /// <summary>
        /// Gets a boolean from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static bool GetBoolean(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<bool>(P_Reader, P_AttributeName, () => default(bool), (x) => Convert.ToBoolean(x, CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Gets a nullable boolean from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static bool? GetNullableBoolean(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<bool?>(P_Reader, P_AttributeName, () => null, (x) => Convert.ToBoolean(x, CultureInfo.InvariantCulture));
        }
        #endregion

        #region Decimal
        /// <summary>
        /// Gets a decimal value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static decimal GetDecimal(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<decimal>(P_Reader, P_AttributeName, () => default(decimal), (x) => Convert.ToDecimal(x, CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Gets a nullable decimal value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static decimal? GetNullableDecimal(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<decimal?>(P_Reader, P_AttributeName, () => null, (x) => Convert.ToDecimal(x, CultureInfo.InvariantCulture));
        }
        #endregion

        #region Integer
        /// <summary>
        /// Gets an integer value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static int GetInteger(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<int>(P_Reader, P_AttributeName, () => default(int), (x) => Convert.ToInt32(x, CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Gets a nullable integer value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static int? GetNullableInteger(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<int?>(P_Reader, P_AttributeName, () => null, (x) => Convert.ToInt32(x, CultureInfo.InvariantCulture));
        }

        #endregion

        #region Double
        /// <summary>
        /// Gets a double value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static double GetDouble(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<double>(P_Reader, P_AttributeName, () => default(double), (x) => Convert.ToDouble(x, CultureInfo.InvariantCulture));
        }
        /// <summary>
        /// Gets a nullable double value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static double? GetNullableDouble(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<double?>(P_Reader, P_AttributeName, () => null, (x) => Convert.ToDouble(x, CultureInfo.InvariantCulture));
        }
        #endregion

        #region String
        /// <summary>
        /// Gets a string value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static string GetString(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<string>(P_Reader, P_AttributeName, () => default(string), (x) => x.ToString());
        }
        #endregion

        #region GUID
        /// <summary>
        /// Gets a guid value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static Guid GetGuid(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<Guid>(P_Reader, P_AttributeName, () => default(Guid), (x) => new Guid(x.ToString()));
        }

        /// <summary>
        /// Gets a nullable integer value from the database flow.
        /// </summary>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        public static Guid? GetNullableGuid(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetValue<Guid?>(P_Reader, P_AttributeName, () => null, (x) => new Guid(x.ToString()));
        }

        #endregion

        #region Enum
        public static T GetEnum<T>(this IDataRecord P_Reader, string P_AttributeName)
        {
            return GetEnum<T>(P_Reader, P_AttributeName, () => default(T));
        }

        public static T? GetNullableEnum<T>(this IDataRecord P_Reader, string P_AttributeName) where T : struct
        {
            return GetEnum<Nullable<T>>(P_Reader, P_AttributeName, () => null);
        }

        /// <summary>
        /// Gets the enum value.
        /// </summary>
        /// <typeparam name="T">Enum type.</typeparam>
        /// <param name="P_Reader">The reader.</param>
        /// <param name="P_AttributeName">The attribute name.</param>
        /// <param name="P_EnumDefault">The enum default.</param>
        public static T GetEnum<T>(this IDataRecord P_Reader, string P_AttributeName, Func<T> P_EnumDefault)
        {
            return GetValue<T>(P_Reader, P_AttributeName, P_EnumDefault, (x) => (T)Enum.Parse(typeof(T), x.ToString().Trim(), true));
        }

        #endregion
        #endregion

        #region Writers

        /// <summary>
        /// Creates a data parameter with the specified values.
        /// </summary>
        /// <typeparam name="T">Value type.</typeparam>
        /// <param name="P_ValueName">Parameter name.</param>
        /// <param name="P_Value">The value.</param>
        /// <param name="P_ParameterCreator">Parameter creator.</param>
        public static IDataParameter PutValue<T>(string P_ValueName, T P_Value, Func<string, IDataParameter> P_ParameterCreator)
        {
            IDataParameter L_Parameter = P_ParameterCreator(P_ValueName);
            if (P_Value == null)
            {
                L_Parameter.Value = DBNull.Value;
            }
            else
            {
                L_Parameter.Value = P_Value;
            }

            return L_Parameter;
        }

        /// <summary>
        /// Creates a data parameter with the specified values.
        /// </summary>
        /// <typeparam name="T">Value type.</typeparam>
        /// <param name="P_ValueName">Parameter name.</param>
        /// <param name="P_Value">The value.</param>        
        /// <param name="P_ParameterCreator">Parameter creator.</param>
        /// <param name="P_DefaultValue">Default value, replaced with DBNull.</param>
        /// <param name="P_ParameterCreator">Parameter creator.</param>
        public static IDataParameter PutValue<T>(string P_ValueName, T P_Value, Func<string, IDataParameter> P_ParameterCreator, Func<T> P_DefaultValue)
        {
            IDataParameter L_Parameter = P_ParameterCreator(P_ValueName);
            if (Equals(P_DefaultValue(), P_Value))
            {
                L_Parameter.Value = DBNull.Value;
            }
            else
            {
                L_Parameter.Value = P_Value;
            }

            return L_Parameter;
        }

        #endregion

        #region Patterns
        public static IEnumerable<T> WrapperTransactional<T>(this IDatabaseAdapter P_Database, Func<IDbTransaction, IEnumerable<T>> P_Wrapped)
        {
            IDbTransaction L_Transaction = null;
            try
            {
                L_Transaction = P_Database.OpenTransaction();
                List<T> L_Return = P_Wrapped(L_Transaction).ToList();
                L_Transaction.Commit();
                return L_Return;
            }
            catch
            {
                if (L_Transaction != null)
                {
                    L_Transaction.Rollback();
                }

                throw;
            }
            finally
            {
                if (L_Transaction != null)
                {
                    L_Transaction.Dispose();
                }
            }
        }

        public static T WrapperTransactional<T>(this IDatabaseAdapter P_Database, Func<IDbTransaction, T> P_Wrapped)
        {
            IDbTransaction L_Transaction = null;
            try
            {
                L_Transaction = P_Database.OpenTransaction();
                T L_Return = P_Wrapped(L_Transaction);
                L_Transaction.Commit();
                return L_Return;
            }
            catch
            {
                if (L_Transaction != null)
                {
                    L_Transaction.Rollback();
                }

                throw;
            }
            finally
            {
                if (L_Transaction != null)
                {
                    L_Transaction.Dispose();
                }
            }
        }

        public static void WrapperTransactional(this IDatabaseAdapter P_Database, Action<IDbTransaction> P_Wrapped)
        {
            IDbTransaction L_Transaction = null;
            try
            {
                L_Transaction = P_Database.OpenTransaction();
                P_Wrapped(L_Transaction);
                L_Transaction.Commit();
            }
            catch
            {
                if (L_Transaction != null)
                {
                    L_Transaction.Rollback();
                }

                throw;
            }
            finally
            {
                if (L_Transaction != null)
                {
                    L_Transaction.Dispose();
                }
            }
        }
        #endregion
    }
}
